#!/usr/bin/env ruby
#
# Helper Script to access the ManageIQ REST API
#
# Makes use of the Optimist and Faraday Client Gems
#   gem install optimist
#   gem install faraday
#   gem install faraday_middleware
#
# Run without options to see the usage.
#
# Globals accessible by loaded test scripts
#

require 'manageiq/api/version'
require 'uri'
require 'json'
require 'optimist'
require 'faraday'
require 'faraday_middleware'

module ManageIQ
  module Api
    API_CMD = File.basename($PROGRAM_NAME)

    class Cli
      METHODS = {
        "get"     => "get",
        "put"     => "put",
        "post"    => "post",
        "patch"   => "patch",
        "edit"    => "post",
        "create"  => "post",
        "update"  => "post",
        "delete"  => "delete",
        "options" => "options",
      }.freeze

      SEP       = ("_" * 60).freeze
      PREFIX    = "/api".freeze
      CTYPE     = "application/json".freeze
      SCRIPTDIR = ENV['API_SCRIPTDIR'] ? ENV['API_SCRIPTDIR'] : ENV['HOME'] + "/bin".freeze

      ACTIONS              = METHODS.keys
      METHODS_NEEDING_DATA = %w(put post patch).freeze
      SCRIPTDIR_ACTIONS    = %w(ls run).freeze
      SUB_COMMANDS         = ACTIONS + %w(edit vi) + SCRIPTDIR_ACTIONS
      API_PARAMETERS       = %w(expand hide attributes decorators limit offset
                                depth search_options format_attributes
                                sort_by sort_order sort_options
                                filter by_tag provider_class collection_class requester_type).freeze

      MULTI_PARAMS         = %w(filter).freeze

      def msg_exit(msg, exit_code = 1)
        puts msg
        exit exit_code
      end

      def json_pretty(json)
        puts JSON.pretty_generate(JSON.parse(json))
      rescue
        puts json
      end

      def prompt_get_data
        data = ""
        puts "Enter data to send with request:"
        puts "Terminate with \"\" or \".\""
        while (s = gets)
          s = s.strip
          break if s == "." || s == ""
          data << s
        end
        data
      end

      def script_filename(scriptdir, filename)
        filename &&= filename.strip
        return if filename.nil? || filename == ""
        filename = "api_#{filename}" unless filename =~ /^api_/
        filename = "#{filename}.rb" unless filename =~ /\.rb$/
        File.join(scriptdir, filename)
      end

      def run
        data      = ""
        params    = {}

        opts = Optimist.options do
          version "#{API_CMD} #{VERSION} - ManageIQ REST API Access Script"
          banner <<-EOS
        #{version}

        Usage: #{API_CMD} [options] <action> [parameters] [resource]

                    action - is the action to use for the request, i.e. get, post, patch, edit ...

                    [parameters] include: #{API_PARAMETERS.join(", ")}
                                 specify --help for additional help

                    [resource] - is the optional resource i.e. services

               #{API_CMD} [options] vi|edit [script]

                    Edit optional api_*.rb scripts.

               #{API_CMD} [options] run script [method]

                    Run optional api_* scripts

               #{API_CMD} [options] ls

                    List optional api_* scripts (without the api_ prefix)

        #{API_CMD} options are:
        EOS
          opt :verbose,    "Verbose mode, show details of the communication",
              :default => false,                    :short => '-v'
          opt :apiversion, "Version of the API to access",
              :default => "",                       :short => '-V'
          opt :url,        "Base URL of Appliance to access",
              :default => "http://localhost:3000",  :short => '-l'
          opt :user,       "User to authenticate as",
              :default => "admin",                  :short => '-u'
          opt :group,      "User group to authorize as",
              :default => "",                       :short => '-g'
          opt :password,   "Password for user specified to authenticate as",
              :default => "smartvm",                :short => '-p'
          opt :token,      "Token to use for authentication instead of user/password",
              :default => "",                       :short => '-t'
          opt :miqtoken,   "Token to use for system authentication",
              :default => "",                       :short => '-m'
          opt :format,     "How to format Json, pretty|none",
              :default => "pretty",                 :short => '-f'
          opt :inputfile,  "File to use as input to the POST/PUT/PATCH methods",
              :default => "",                       :short => '-i'
          opt :scriptdir,  "Directory where optional api_* scripts live",
              :default => SCRIPTDIR,                :short => '-s'
          stop_on SUB_COMMANDS
        end

        unless opts[:inputfile].empty?
          Optimist.die :inputfile, "File specified #{opts[:inputfile]} does not exist" unless File.exist?(opts[:inputfile])
        end

        begin
          URI.parse(opts[:url])
        rescue
          Optimist.die :url, "Invalid URL syntax specified #{opts[:url]}"
        end

        action = ARGV.shift
        Optimist.die "Must specify an action" if action.nil?

        if SCRIPTDIR_ACTIONS.include?(action)
          msg_exit("Script directory #{opts[:scriptdir]} does not exist") unless File.directory?(opts[:scriptdir])
        end

        if action == "ls"
          d = Dir.open(opts[:scriptdir])
          d.each do |file|
            p = file.scan(/^api_(.*)\.rb/)
            puts p unless p.nil?
          end
          d.close
          exit 0
        end

        if action == "vi" || action == "edit"
          api_script_file = script_filename(opts[:scriptdir], ARGV.shift) || File.expand_path($PROGRAM_NAME)
          ed_cmd = "vi"
          ed_cmd = ENV["EDITOR"] if action == "edit" && ENV["EDITOR"]
          cmd = "#{ed_cmd} #{api_script_file}"
          system(cmd)
          exit 0
        end

        if action == "run"
          script = ARGV.shift
          method = ARGV.shift
          api_script = script_filename(opts[:scriptdir], script)
          msg_exit("Must specify a script to run.") if api_script.nil?
          msg_exit("Script file #{api_script} does not exist") unless File.exist?(api_script)
        else
          api_params = Optimist.options do
            norm_options  = {:default => ""}
            multi_options = {:default => "", :multi => true}
            API_PARAMETERS.each { |p| opt p.intern, p, (MULTI_PARAMS.include?(p) ? multi_options.dup : norm_options.dup) }
          end
          API_PARAMETERS.each do |param|
            if MULTI_PARAMS.include?(param)
              params[param] = api_params[param.intern] unless api_params[param.intern].all?(&:empty?)
            else
              params[param] = api_params[param.intern] unless api_params[param.intern].empty?
            end
          end
        end

        if action != "run"
          resource = ARGV.shift

          resource = "/" + resource             if resource && resource[0] != "/"
          resource = resource.gsub(PREFIX, '')  unless resource.nil?

          method = METHODS[action]
          msg_exit("Unsupported action #{action} specified") if method.nil?

          if METHODS_NEEDING_DATA.include?(method)
            data = opts[:inputfile].empty? ? prompt_get_data : File.read(opts[:inputfile])

            msg_exit("Action #{action} requires data to be specified") if data.empty?
          end
        end

        conn = Faraday.new(:url => opts[:url], :ssl => {:verify => false}) do |faraday|
          faraday.request(:url_encoded)               # form-encode POST params
          faraday.response(:logger) if opts[:verbose] # log requests to STDOUT
          faraday.use FaradayMiddleware::FollowRedirects, :limit => 3, :standards_compliant => true
          faraday.adapter(Faraday.default_adapter)    # make requests with Net::HTTP
          faraday.basic_auth(opts[:user], opts[:password]) if opts[:token].empty? && opts[:miqtoken].empty?
        end

        if action == "run"
          puts "Loading #{api_script}"
          require api_script
          as = ApiScript.new(CTYPE, conn)
          puts "Running #{api_script} with method #{method} ..."
          method.nil? ? as.run : as.run(method)
          exit
        end

        path = PREFIX.dup
        path << "/v#{opts[:apiversion]}" unless opts[:apiversion].empty?

        collection = ""
        item = ""
        unless resource.nil?
          path << resource
          rscan = resource.scan(%r{[^/]+})
          collection, item = rscan[0..1]
        end

        if opts[:verbose]
          puts SEP
          puts "Connection Endpoint: #{opts[:url]}"
          puts "Action:              #{action}"
          puts "HTTP Method:         #{method}"
          puts "Resource:            #{resource}"
          puts "Collection:          #{collection}"
          puts "Item:                #{item}"
          puts "Parameters:"
          params.keys.each { |k| puts "#{' ' * 21}#{k} = #{params[k]}" }
          puts "Path:                #{path}"
          puts "Data:                #{data}"
        end

        begin
          response = conn.run_request(method.to_sym, path, nil, nil) do |req|
            req.headers[:content_type]  = CTYPE
            req.headers[:accept]        = CTYPE
            req.headers['X-MIQ-Group']  = opts[:group] unless opts[:group].empty?
            req.headers['X-MIQ-Token']  = opts[:miqtoken] unless opts[:miqtoken].empty?
            req.headers['X-Auth-Token'] = opts[:token] unless opts[:token].empty?
            req.params.merge!(params)
            req.body = data if METHODS_NEEDING_DATA.include?(method)
          end
        rescue => e
          msg_exit("\nFailed to connect to #{opts[:url]} - #{e}")
        end

        if opts[:verbose]
          puts SEP
          puts "Response Headers:"
          puts response.headers

          puts SEP
          puts "Response Body:"
        end

        if response.body
          body = response.body.strip
          if opts[:format] == "pretty"
            puts json_pretty(body) unless body.empty?
          else
            puts body
          end
        end

        exit response.status >= 400 ? 1 : 0
      end
    end
  end
end

ManageIQ::Api::Cli.new.run
